package com.nulldev.util.web.YouTubeExtractor.channel;

import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.CLIENT_VERSION;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_STS;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.channel.ChannelUtil.__ENSURE_ARRAY;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.DURATION_TYPE_SHOW_AS_SHORT;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getTabRendererWithContent;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.time.Duration;
import java.util.List;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.internal.backport.concurrency9.Maps;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.channel.ChannelExtractor.Channel;
import com.nulldev.util.web.YouTubeExtractor.channel.ChannelExtractor.Video;
import com.nulldev.util.web.YouTubeExtractor.util.YTRequestObject;

public class ChannelSubpageExtractor {

	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--debug", "-debug", "--trace", "--ChannelExtractor.debug"));

	public static enum SubPageType {

		LIVESTREAMS,
		SHORTS,

	}

	public static Channel extract(String channelID, final String continuation, final SubPageType pageType) {
		return extract(channelID, continuation, pageType, true);
	}

	public static Channel extract(String channelID, final String continuation, final SubPageType pageType, final boolean sendConsentCookie) {
		Variables.requireNonNull(channelID, "channelID");
		Variables.requireNonNull(pageType, "pageType");
		final String URL;
		final boolean hasCont = continuation != null && !StringsUtil.isBlank(continuation);
		switch (pageType) {
			case SHORTS: {
				if (hasCont) {
					URL = "https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false";
					break;
				} else {
					URL = "https://www.youtube.com/channel/" + channelID + "/shorts?pbj=1";
					break;
				}
			}
			case LIVESTREAMS: {
				if (hasCont) {
					URL = "https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false";
					break;
				} else {
					URL = "https://www.youtube.com/channel/" + channelID + "/streams?pbj=1";
					break;
				}
			}
			default: {
				throw new RuntimeException("unreachable: Invalid page type: " + pageType);
			}
		}
		try {
			String jpl = null;
			if (hasCont) {
				if (continuation != null && !continuation.isEmpty()) {
					final UniJsonValue o = YTRequestObject.makeYTRequestObject(Maps.of());
					o.add("continuation", continuation);
					final String op = o.toString();
					jpl = op;
				}
			}
			final String _result = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(15))
					.headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES : YouTubeExtractor.BLANK_COOKIES).header("X-YouTube-Client-Name", "1")
					.header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL)
					.header("X-YouTube-STS", PAGE_STS).header("X-YouTube-Utc-Offset", UTC_OFFSET).header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM)
					.header(hasCont ? new Entry("Content-Type", "application/json") : null).create(hasCont ? HttpMethod.POST : HttpMethod.GET, jpl)
					.dataThenClose();
			if (_result.startsWith("[") || _result.endsWith("]")) {
				final UniJsonValue uiv = UniJsonValue.parseArray(_result);
				final UniJsonValue _contents = uiv.arrayGetContains("response");
				final UniJsonValue contents = _contents.get("response").get("contents");
				final UniJsonValue tabs = contents.get("twoColumnBrowseResultsRenderer").get("tabs");
				final UniJsonValue section = getTabRendererWithContent(tabs.asArray());
				Variables.requireNonNull(section, "section");
				final UniJsonValue _renderer = section.get("tabRenderer").get("content");

				final UniJsonValue renderer;
				if (_renderer.contains("richGridRenderer")) {
					renderer = _renderer.get("richGridRenderer");
				} else if (_renderer.contains("sectionListRenderer")) {
					renderer = _renderer.get("sectionListRenderer");
				} else {
					log.error("Unknown section: " + section);
					throw new RuntimeException();
				}

				final UniJsonValue rendererContents = renderer.get("contents");
				final List<Video> videos = FastLists.list();
				final variable<String> cont = new variable<String>();

				__do_renderer_contents(rendererContents, videos, cont);

				return new Channel(videos, Lists.of(), Lists.of(), channelID, -1).setContinuation(cont.get());
			} else if (_result.startsWith("{") || _result.endsWith("}")) {
				final UniJsonValue uiv = UniJsonValue.parse(_result);
				if (!uiv.contains("response")) {
					log.error("Unknown data object, missing a response object!");
					throw new UnsupportedOperationException("Unimplemented alt response type");
				}
				final UniJsonValue response = uiv.get("response");
				if (!response.contains("contents")) {
					log.error("Unknown response object, missing a contents object!");
					throw new UnsupportedOperationException("Unimplemented alt response type");
				} else {
					final UniJsonValue contents = response.get("contents");
					if (!contents.contains("twoColumnBrowseResultsRenderer")) {
						log.error("Unknown response/contents type!");
						throw new UnsupportedOperationException("Unimplemented contents type");
					}
					final UniJsonValue renderer = contents.get("twoColumnBrowseResultsRenderer");
					final UniJsonIterableValue tabs = renderer.get("tabs").asArray();
					final UniJsonValue playlistContent = getTabRendererWithContent(tabs);
					if (playlistContent == null) {
						log.error("Failed to get subpage from tab renderer!");
						throw new RuntimeException();
					}
					final UniJsonValue _entries = playlistContent.get("tabRenderer").get("content");
					if (!_entries.contains("sectionListRenderer", "richGridRenderer")) {
						log.error("Failed to get sectionListRenderer from tab renderer!");
						log.derror(_entries);
						throw new RuntimeException();
					}

					if (_entries.contains("richGridRenderer")) {
						final UniJsonValue entries = _entries.getEither("richGridRenderer").get("contents");
						__ENSURE_ARRAY(entries);

						final List<Video> videos = FastLists.list();
						final variable<String> cont = new variable<String>();
						__do_renderer_contents(entries, videos, cont);

						return new Channel(videos, Lists.of(), Lists.of(), channelID, -1).setContinuation(cont.get());
					} else {
						final UniJsonValue entries = _entries.getEither("sectionListRenderer").get("contents");
						__ENSURE_ARRAY(entries);
						final UniJsonValue first = entries.first();
						if (first.contains("richGridRenderer")) {
							final UniJsonValue richGridRenderer = first.get("richGridRenderer");
							final UniJsonIterableValue richGridRendererContents = richGridRenderer.get("contents").asArray();

							final List<Video> videos = FastLists.list();
							final variable<String> cont = new variable<String>();
							__do_renderer_contents(richGridRendererContents, videos, cont);

							return new Channel(videos, Lists.of(), Lists.of(), channelID, -1).setContinuation(cont.get());
						} else if (first.contains("itemSectionRenderer")) {
							final UniJsonValue itemSectionRenderer = first.get("itemSectionRenderer");
							final UniJsonIterableValue itemSectionRendererContents = itemSectionRenderer.get("contents").asArray();

							final List<Video> videos = FastLists.list();
							final variable<String> cont = new variable<String>();
							__do_renderer_contents(itemSectionRendererContents, videos, cont);

							return new Channel(videos, Lists.of(), Lists.of(), channelID, -1).setContinuation(cont.get());
						} else {
							log.error("Unknown object: " + first);
							throw new RuntimeException();
						}
					}
				}
			} else {
				throw new UnsupportedOperationException("Unimplemented alt response type");
			}
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	private static void __do_renderer_contents(final UniJsonValue rendererContents, final List<Video> videos, final variable<String> cont) {
		for (final UniJsonValue rc : rendererContents.asArray()) {
			/* "itemSectionRenderer" suggests that there probably isn't any shorts... */
			if (!rc.contains("richItemRenderer", "continuationItemRenderer", "channelVideoPlayerRenderer")) {
				log.dwarn("[__do_renderer_contents] Unknown object: " + rc);
				continue;
			} else if (rc.contains("continuationItemRenderer")) {
				final UniJsonValue continuationItemRenderer = rc.get("continuationItemRenderer");
				cont.set(continuationItemRenderer.get("continuationEndpoint").get("continuationCommand").getString("token"));
			} else if (rc.contains("channelVideoPlayerRenderer")) {
				final UniJsonValue channelVideoPlayerRenderer = rc.get("channelVideoPlayerRenderer");
				// Shameless copy from ChannelExtractor.
				String videoTitle = null;
				try {
					videoTitle = getYTString(channelVideoPlayerRenderer.get("title"));
				} catch (Exception e) {
					log.error(e);
					channelVideoPlayerRenderer.println();
					videoTitle = "N/A";
				}
				if (videoTitle == null) {
					log.warn("videoTitle is null in: " + channelVideoPlayerRenderer);
				}
				videos.add(new Video(videoTitle, channelVideoPlayerRenderer.getString("videoId"), DURATION_TYPE_SHOW_AS_SHORT));
			} else {
				final UniJsonValue trueObj = rc.get("richItemRenderer").get("content");
				if (!trueObj.contains("reelItemRenderer", "videoRenderer")) {
					log.warn("Unknown content: " + trueObj);
					continue;
				} else if (trueObj.contains("reelItemRenderer")) {
					final UniJsonValue reelItemRenderer = trueObj.get("reelItemRenderer");
					final String title = getYTString(reelItemRenderer.get("headline"));
					final String id = reelItemRenderer.getString("videoId");
					videos.add(new Video(title, id, DURATION_TYPE_SHOW_AS_SHORT));
				} else {
					final UniJsonValue video = trueObj.get("videoRenderer");
					// Shameless copy from ChannelExtractor.
					String videoTitle = null;
					try {
						videoTitle = getYTString(video.get("title"));
					} catch (Exception e) {
						log.error(e);
						video.println();
						videoTitle = "N/A";
					}
					if (videoTitle == null) {
						log.warn("videoTitle is null in: " + video);
					}
					String time = "0:00";
					try {
						time = getYTString(video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
								.get("thumbnailOverlayTimeStatusRenderer").get("text"));
					} catch (Exception e) {
						log.error("Failed to get time. Data: " + video.toString(), e);
					}
					if (time.equalsIgnoreCase("SHORTS")) {
						// We can't do anything, here just set it to the max time that YT shorts can
						// last.
						time = ChannelExtractor.YT_SHORTS_TIME_LIMIT;
					}
					String animatedThumbnail = null;
					try {
						if (video.contains("richThumbnail")) {
							final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
							if (o.contains("movingThumbnailDetails")) {
								animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
							}
						}
					} catch (Exception e) {
						log.error(e);
					}
					long startTime = -1L;
					boolean isLive = false;
					boolean isUpcoming = false;
					try {
						if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style").equalsIgnoreCase("LIVE"))
							isLive = true;
					} catch (Exception e) {
					}
					try {
						if (video.contains("upcomingEventData")) {
							isUpcoming = true;
							startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
						}
					} catch (Exception e) {
						log.error("Failed to check for upcoming video!", e);
					}
					try {
						videos.add(new Video(videoTitle, video.getString("videoId"), isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
								.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
					} catch (Exception e) {
						log.error("Failed to add video! [isLive: " + isLive + "]", e);
						log.debug(video);
					}
				}
			}
		}
	}

	public static void main(String[] args) {
		extract("UCvcRA2Hva1lULVf4GCouH8w", null, SubPageType.LIVESTREAMS);
	}
}
